declare var window: any;
import { _decorator, assetManager, Color, gfx, Material, Mesh, MeshRenderer, Node, Texture2D, Vec3, primitives, Layers, Asset, Mat4, Quat, BufferAsset, ImageAsset } from 'cc';
import {  GLTF, GLTFBase, glTFParser, Node as GNode } from './glTFParser';
import { MeshUtils } from './MeshUtils';
import { glTFSettings } from './glTFSettings';
import { NATIVE } from 'cc/env';
import { arrayBufferToString } from './weapp-ext';
const { ccclass, property } = _decorator;
const WARP_MODE_MAP = { 10497: 0, 33071: 2, 33648: 1, 33069: 3 }, FILTER_MODE_MAP = { 9728: 0, 9729: 2, 9987: 0, 9984: 0, 9985: 2, 9986: 1 },TEMP_VEC3 = new Vec3(), TEMP_QUAT = new Quat();

if (NATIVE || !Asset.prototype.hasOwnProperty('nativeAsset')) {
    Object.defineProperty(Asset.prototype, 'nativeAsset', {
        get() { return this._nativeAsset || this._file; },
        set(v) { this._nativeAsset = v; }
    });
}

@ccclass('glTFLoader')
export class glTFLoader {
    private static _colorMaterial: Material = null;
    private static _defaultMaterial: Material = null;
    private static _initialed = false;

    private static getBaseUri(uri: string) { let i = uri.lastIndexOf('/'); return i !== -1 ? uri.substring(0, i + 1) : ''; }

    public static releaseAsset(a: Asset | string) {
        if (!a) return;
        if (typeof a === 'string') {
            a = assetManager.assets.get(a);
            if (!a) return;
        }
        //@ts-ignore
        let d = a.nativeAsset, json = d.json as GLTFBase;
        if (!json) return;
        let imgs = json.images;
        if (imgs) for (let i = 0; i < imgs.length; i++) { let img = imgs[i]; if (img.texture) { assetManager.releaseAsset(img.texture); img.texture = null; } }
        let gltf = d.gltf as GLTF;
        if (gltf) {
            let mats = gltf.materials;
            if (mats) for (let i = 0; i < mats.length; i++) { let m = mats[i]; if (m) { assetManager.releaseAsset(m.material); m.material = null; } }
        }
        if (d.model) (d.model as Node).destroy();
        if (json.assets) { for (let t of json.assets) assetManager.releaseAsset(t); json.assets = null; }
        assetManager.releaseAsset(a);
    }

    private static _getFileNameWithoutExtension(u: string) {
        let i = u.lastIndexOf('/'); if (i === -1) i = u.lastIndexOf('\\');
        let n = u.substring(i + 1); i = n.lastIndexOf('.');
        return i !== -1 ? n.substring(0, i) : n;
    }

    private static _parseBuffer(url: string, file: ArrayBuffer, cb: (err: any, dt: any) => void) {
        let v = new DataView(file), magic = v.getUint32(0, true);
        if (magic !== 0x46546C67) { cb(new Error('Invalid glb file'), null); return; }
        let ver = v.getUint32(4, true);
        if (ver !== 2) { cb(new Error('Unsupported version'), null); return; }
        let jsonLen = v.getUint32(12, true), jsonOff = 20, jsonChunk = new Uint8Array(file, jsonOff, jsonLen), jsonStr: string = null;
        if (typeof TextDecoder !== 'undefined') jsonStr = new TextDecoder().decode(jsonChunk);
        else jsonStr = arrayBufferToString(jsonChunk);
        let json = JSON.parse(jsonStr), baseUri = this.getBaseUri(url), byteOff = jsonOff + jsonLen, buffers = json.buffers;
        byteOff += 8;
        for (let i = 0; i < buffers.length; i++) {
            let b = buffers[i], bl = b.byteLength;
            b.data = new ArrayBuffer(bl);
            new Uint8Array(b.data).set(new Uint8Array(file, byteOff, bl));
            byteOff += bl;
        }
        json.name = this._getFileNameWithoutExtension(url);
        this._parseglTFJson(baseUri, json, cb);
    }

    public static initial() {
        if (this._initialed) return;
        this._initialed = true;
        assetManager.transformPipeline.append((task: any) => {
            const input = task.output = task.input;
            for (let ipt of input) {
                if (ipt.ext === '.glb') { ipt.ext = '.bin'; ipt.options = ipt.options || {}; ipt.options.__fromGlb = true; ipt.options.__url = ipt.url; }
                else if (ipt.ext === '.gltf') { ipt.ext = '.json'; ipt.options = ipt.options || {}; ipt.options.__fromGltf = true; ipt.options.__url = ipt.url; }
            }
        });
        assetManager.parser.register('.json', (file, options, cb) => {
            if (options.__fromGltf) {
                file.name = this._getFileNameWithoutExtension(options.__url);
                const baseUri = this.getBaseUri(options.__url);
                this._parseglTFJson(baseUri, file, (err, data) => {
                    if (err) { cb(err, null); return; }
                    glTFLoader._parseGLTFModel(data, cb);
                });
            } else cb(null, file);
        });
        assetManager.parser.register('.bin', (file, options, cb) => {
            if (options.__fromGlb) {
                const proc = (buffer) => {
                    glTFLoader._parseBuffer(options.__url, buffer, (err, data) => {
                        data.name = this._getFileNameWithoutExtension(options.__url);
                        if (err) { cb(err, null); return; }
                        glTFLoader._parseGLTFModel(data, cb);
                    });
                };
                if (typeof file === 'string') {
                    //@ts-ignore
                    window.fsUtils.readArrayBuffer(file, (err, buffer) => { if (err) { cb(err, null); return; } proc(buffer); });
                } else proc(file);
            } else {
                if (typeof file === 'string') {
                    //@ts-ignore
                    window.fsUtils.readArrayBuffer(file, cb);
                } else cb(null, file);
            }
        });
    }

    private static _parseImageFromBuffer(gltf: GLTF, imgInfo: { bufferView: number, mimeType: string }, cb?: (err: any, dt: any) => void) {
        let bv = gltf.bufferViews[imgInfo.bufferView], buf = bv.data;
        if (!NATIVE && typeof Blob !== 'undefined') {
            let blob = new Blob([buf], { type: imgInfo.mimeType }), url = URL.createObjectURL(blob), img = new Image();
            img.src = url; img.onload = () => { if (cb) cb(null, img); };
        } else if (typeof btoa !== 'undefined') {
            let base64 = btoa(arrayBufferToString(new Uint8Array(buf))), img = new Image();
            img.src = 'data:' + imgInfo.mimeType + ';base64,' + base64; img.onload = () => { if (cb) cb(null, img); };
        } else cb(new Error("can not decode image from Buffer"), null);
    }

    private static _loadBufferImages(file: any, gltf: GLTF, cb: (err: any, res: any) => void) {
        let imgs = file.images; if (!imgs) { cb(null, null); return; }
        imgs = imgs.filter(img => img.bufferView !== undefined);
        if (imgs.length === 0) { cb(null, null); return; }
        let cnt = 0, err = null, total = imgs.length, results = [], complete = (e, d) => { if (e) err = e; results.push(d); cnt++; if (cnt === total) { if (err) cb(err, null); else cb(null, results); } };
        for (let i = 0; i < imgs.length; i++) {
            let img = imgs[i];
            if (img.bufferView !== undefined) this._parseImageFromBuffer(gltf, img, (e, d) => { img.data = d; complete(e, d); });
            else complete(null, null);
        }
    }

    private static _parseGLTFModel(file: any, cb: (err: any, res: any) => void) {
        let parser = new glTFParser(), gltf = parser.proParse(file);
        if (!gltf) { cb(new Error('Invalid glTF file'), null); return; }
        this._loadBufferImages(file, gltf, (err, data) => {
            this._parseImages(file, gltf);
            parser.parse();
            this._createMaterials(gltf);
            let res = this._createModel(file, gltf);
            cb(null, res);
        });
    }

    private static _parseglTFJson(baseUri: string, file: any, cb: (err: any, data: any) => void) {
        let resources = [], resMap = file.resMap = {}, imgs = file.images;
        if (imgs) for (let i = 0; i < imgs.length; i++) {
            const img = imgs[i]; if (!img.uri) continue;
            let url = img.uri, isBase64 = url.startsWith('data:');
            if (!url.startsWith('http') && !isBase64) url = baseUri + url;
            let ext = url.substr(url.lastIndexOf('.'));
            resources.push({ url, type: ext }); resMap[url] = { index: i, type: 'images', isBase64 };
        }
        let buffers = file.buffers;
        if (buffers) for (let i = 0; i < buffers.length; i++) {
            const buffer = buffers[i];
            if (!buffer.uri) continue;
            let url = buffer.uri || "", isBase64 = url.startsWith('data:');
            if (!isBase64 && !url.startsWith('http')) url = baseUri + url;
            let ext = url.substr(url.lastIndexOf('.'));
            resources.push({ url, type: ext }); resMap[url] = { index: i, type: 'buffers', isBase64 };
        }
        if (resources.length === 0) { cb(null, file); return; }
        let cnt = 0, err = null, assets = [file], total = resources.length, complete = (url, e, a) => {
            if (e) err = e;
            let res = resMap[url];
            if (res.type === 'images') {
                if (a instanceof ImageAsset) { file.images[res.index].data = a.data; assets.push(a); }
                else if (a instanceof Image) file.images[res.index].data = a;
            } else if (res.type === 'buffers') {
                if (a instanceof BufferAsset) { file.buffers[res.index].data = a._nativeAsset; assets.push(a); }
                else if (a instanceof ArrayBuffer) file.buffers[res.index].data = a;
            }
            cnt++;
            if (cnt === total) { if (err) cb(err, null); else cb(null, file); }
        };
        for (let i = 0; i < resources.length; i++) {
            let resource = resources[i], res = resMap[resource.url];
            if (res.isBase64) {
                if (res.type === "images") {
                    let img = new Image(); img.src = resource.url; img.onload = () => { complete(resource.url, null, img); };
                } else if (res.type === "buffers") {
                    let url = resource.url, base64 = url.split(',')[1], bin = atob(base64), len = bin.length, buf = new ArrayBuffer(len), view = new Uint8Array(buf);
                    for (let j = 0; j < len; j++)view[j] = bin.charCodeAt(j);
                    complete(resource.url, null, buf);
                }
            } else {
                let opt: any = {};
                assetManager.loadRemote(resource.url, opt, (e, d) => { complete(resource.url, e, d); });
            }
        }
    }

    private static _parseImages(gltfBase: any, gltf: GLTF) {
        let imgs = gltfBase.images, txs = gltfBase.textures;
        if (!imgs || !txs) return;
        let samplers = gltfBase.samplers, newImgs = [];
        for (let i = 0; i < txs.length; i++) {
            let t = txs[i], img = imgs[t.source];
            newImgs.push(img);
            if (img.texture) continue;
            let d = img.data, tex = new Texture2D();
            tex.name = img.name; tex.reset({ width: d.width, height: d.height, });
            if (samplers && t.sampler) {
                let s = samplers[t.sampler];
                if (s.wrapS || s.wrapT) tex.setWrapMode(WARP_MODE_MAP[s.wrapS] || 0, WARP_MODE_MAP[s.wrapT] || 0);
                if (s.minFilter || s.magFilter) {
                    tex.setFilters(FILTER_MODE_MAP[s.minFilter] || 0, FILTER_MODE_MAP[s.magFilter] || 0);
                    tex.setMipFilter(s.minFilter >= 9984 ? FILTER_MODE_MAP[s.minFilter] || 0 : FILTER_MODE_MAP[s.magFilter] || 0);
                }
            }
            tex.uploadData(d); img.texture = tex;
        }
        gltfBase.images = newImgs;
    }

    public static getTexture(gltf: GLTF, index: number) {
        if (index < 0) return null; let t = gltf.textures[index];
        return t || null;
    }

    private static _createMaterials(gltf: GLTF) {
        if (!gltf.materials) return;
        let mats = gltf.materials;
        for (let i = 0; i < mats.length; i++) {
            let m = mats[i], mat: Material = null;
            if (glTFSettings.beforeCreateMaterial) {
                mat = glTFSettings.beforeCreateMaterial(gltf, m);
                if (mat) { m.material = mat; if (glTFSettings.afterCrateMaterial) m.material = glTFSettings.afterCrateMaterial(gltf, m, mat); continue; }
            }
            mat = new Material(); mat.name = m.name;
            let pbr = m.pbrMetallicRoughness;
            mat.initialize({
                effectAsset: glTFSettings.instance.unlitEffect,
                defines: { USE_TEXTURE: !!pbr.baseColorTexture, USE_ALPHA_TEST: m.alphaMode == 'MASK', },
                technique: m.alphaMode == 'BLEND' ? 1 : 0,
            });
            if (pbr.baseColorTexture) {
                let tex = this.getTexture(gltf, pbr.baseColorTexture.index);
                mat.setProperty('mainTexture', tex);
            }
            if (pbr.baseColorFactor) {
                let c = new Color(pbr.baseColorFactor[0] * 255, pbr.baseColorFactor[1] * 255, pbr.baseColorFactor[2] * 255, pbr.baseColorFactor[3] * 255);
                mat.setProperty('mainColor', c);
            }
            m.material = mat;
            if (glTFSettings.afterCrateMaterial) m.material = glTFSettings.afterCrateMaterial(gltf, m, mat);
        }
    }

    private static _createModel(gltfBase: any, gltf: GLTF) {
        let nodeMap = new Map<number, GNode>();
        if (!gltf.nodes) return;
        let skinNodes: number[] = [];
        let model = new Node();
        model.name = gltfBase.name || "glTF Node";
        model.layer = Layers.Enum.DEFAULT;
        let sceneNodes = gltf.scene.nodes;
        for (let i = 0; i < sceneNodes.length; i++) {
            let n = sceneNodes[i];
            this._createNode(gltf, n, model, nodeMap, skinNodes);
        }
        return { json: gltfBase, gltf, model };
    }

    private static _setTransformFromMatrix(node: Node, m: Mat4) {
        m.getTranslation(TEMP_VEC3); node.setPosition(TEMP_VEC3);
        m.getRotation(TEMP_QUAT); node.setRotation(TEMP_QUAT);
        m.getScale(TEMP_VEC3); node.setScale(TEMP_VEC3);
    }

    private static _createNode(gltf: GLTF, node: GNode, parent: Node, nodeMap: Map<number, GNode>, skinNodes: number[]) {
        let n = new Node();
        n.layer = parent ? parent.layer : Layers.Enum.DEFAULT;
        n.name = node.name || 'node-' + node.nodeID;
        this._setTransformFromMatrix(n, node.matrix);
        this._createMesh(gltf, n, node, skinNodes);
        if (parent) parent.addChild(n);
        node.node = n; nodeMap.set(node.nodeID, node);
        if (node.children) for (let i = 0; i < node.children.length; i++) {
            let c = node.children[i]; this._createNode(gltf, c, n, nodeMap, skinNodes);
        }
    }

    private static _createMesh(gltf: GLTF, node: Node, gNode: GNode, skinNodes: number[]) {
        if (!gNode.mesh) return;
        let gMesh = gNode.mesh, meshRenderer = node.addComponent(MeshRenderer);
        meshRenderer.receiveShadow = 1;
        let mesh: Mesh = null, geos: primitives.IDynamicGeometry[] = [];
        for (let i = 0; i < gMesh.primitives.length; i++) {
            const p = gMesh.primitives[i], pos = p.attributes.POSITION.loadArrayData() as Float32Array, nor = p.attributes.NORMAL.loadArrayData() as Float32Array, uv = p.attributes.TEXCOORD_0?.loadArrayData() as Float32Array, idx = p.indices.loadArrayData() as Uint16Array | Uint32Array;
            let tan = p.attributes.TANGENT ? p.attributes.TANGENT.loadArrayData() as Float32Array : null;
            if (p.material && p.material.normalTexture && !tan) tan = MeshUtils.calculateTangents(pos, nor, uv, idx);
            if (p.targets) for (let j = 0; j < p.targets.length; j++) {
                const w = gMesh.weights[j]; if (w === 0) continue;
                let tgt = p.targets[j];
                if (tgt.POSITION) { let pts = tgt.POSITION.loadArrayData() as Float32Array; for (let k = 0; k < pts.length; k++)pos[k] += pts[k] * w; }
                if (tgt.NORMAL) { let nors = tgt.NORMAL.loadArrayData() as Float32Array; for (let k = 0; k < nors.length; k++)nor[k] += nors[k] * w; }
                if (tgt.TANGENT) { let tgts = tgt.TANGENT.loadArrayData() as Float32Array; for (let k = 0; k < tgts.length; k++)tan[k] += tgts[k] * w; }
            }
            const col = p.attributes.COLOR_0 ? p.attributes.COLOR_0.loadArrayData() as Float32Array : null;
            let useU16 = idx instanceof Uint16Array, min = p.boundingBox.min, max = p.boundingBox.max;
            let geo = { primitiveMode: gfx.PrimitiveMode.TRIANGLE_LIST, positions: pos, normals: nor, tangents: tan, colors: col, uvs: uv, minPos: min, maxPos: max, indices16: null, indices32: null };
            if (useU16) geo.indices16 = idx; else geo.indices32 = idx;
            geos.push(geo);
        }
        mesh = MeshUtils.createDynamicMesh(geos, mesh);
        meshRenderer.mesh = mesh;
        let mats: Material[] = [];
        for (let i = 0; i < gMesh.primitives.length; i++) {
            let p = gMesh.primitives[i];
            if (p.material) mats.push(p.material.material);
            else {
                if (!this._colorMaterial) {
                    this._colorMaterial = new Material();
                    this._colorMaterial.initialize({ effectAsset: glTFSettings.instance.unlitEffect, defines: { USE_VERTEX_COLOR: true } });
                    this._defaultMaterial = new Material();
                    this._defaultMaterial.initialize({ effectAsset: glTFSettings.instance.unlitEffect, defines: {} });
                }
                let mat = p.attributes.COLOR_0 ? this._colorMaterial : this._defaultMaterial;
                mats.push(mat);
            }
        }
        meshRenderer.sharedMaterials = mats;
    }
}
glTFLoader.initial();